Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.

...powered by www.netzwerkartist.de...

 <<   zurück
Visual Basic 2005 von Andreas Kühnel
Das umfassende Handbuch
Buch: Visual Basic 2005

Visual Basic 2005
1.233 S., mit 2 CDs, 59,90 Euro
Galileo Computing
ISBN 3-89842-585-1
gp Kapitel 11 Multithreading und asynchrone Methodenaufrufe
  gp 11.1 Prozesse und Threads
    gp 11.1.1 Multithreading
    gp 11.1.2 Thread-Zustände und Prioritäten
    gp 11.1.3 Einsatz von mehreren Threads
  gp 11.2 Die Entwicklung einer Multithreading-Anwendung
    gp 11.2.1 Die Klasse »Thread«
    gp 11.2.2 Threadpools nutzen
  gp 11.3 Die Synchronisation von Threads
    gp 11.3.1 Unsynchronisierte Threads
    gp 11.3.2 Der »Monitor« zur Synchronisation
    gp 11.3.3 Das Synchronisationsobjekt »Mutex«
    gp 11.3.4 Das Attribut »MethodImpl«
  gp 11.4 Asynchrone Methodenaufrufe
    gp 11.4.1 Eine kleine Einführung
    gp 11.4.2 Asynchroner Methodenaufruf
    gp 11.4.3 Asynchroner Aufruf mit Rückgabewerten
    gp 11.4.4 Eine Klasse mit asynchronen Methodenaufrufen


Galileo Computing

11.4 Asynchrone Methodenaufrufe  downtop


Galileo Computing

11.4.1 Eine kleine Einführung  downtop

Wird aus einer Methode A heraus die Methode B aufgerufen, wird A erst dann mit den Operationen fortfahren, wenn B vollständig abgearbeitet ist. Die Ausführung der beiden Methoden erfolgt hintereinander, was als synchron bezeichnet wird. Synchrone Operationen haben einen gravierenden Nachteil, denn solange die Methode B ausgeführt wird, ist die Methode A blockiert. Um diese Problematik zu vermeiden, sollten beide Methoden asynchron, d. h., parallel nebeneinander operieren.

Asynchrone Bearbeitung setzt mindestens zwei Threads voraus. Sie haben auf den vergangenen Seiten die wichtigsten Techniken kennen gelernt, um mit Threads zu arbeiten. Sie wissen nun, wie Sie Threads erzeugen und diese möglicherweise synchronisieren können, damit Elemente nicht in einem ungültigen Zustand hinterlassen werden. Ihnen dürfte dabei nicht entgangen sein, dass die Technik sehr komplex ist und einer genauen Planung bedarf, um keine unbeabsichtigten und bösen Überraschungen zu erleben.

Sicherlich erinnern Sie sich noch an das Beispiel der Pumpeneinschaltung in Abschnitt 7.4.1, als wir uns mit den Delegaten auseinander gesetzt haben. Die höchste Entwicklungsstufe hatten wir erreicht, als es uns gelungen war, den Client mittels Rückruf vom erfolgreichen Einschaltvorgang zu benachrichtigen (Beispiel SimpleDelegate). Die Lösung funktionierte tadellos, war aber – wenn man es ganz ehrlich und selbstkritisch beurteilt – unbefriedigend, denn sie setzte voraus, dass der Einschaltvorgang nur eine kurze Zeitspanne dauert. Das ist bei mechanischen Objekten sicherlich nicht gewährleistet. Um der Realität besser zu entsprechen, muss die Klassendefinition der Pumpe noch einmal überarbeitet werden.

Auch in der .NET-Klassenbibliothek finden sich sehr viele Klassen, die Dienste anbieten, deren Ausführung möglicherweise länger dauern kann. Die Dateioperationen zum Lesen und Schreiben zählen dazu. Betrachten wir dazu exemplarisch die Klasse FileStream, die das Schreiben in eine Datei bzw. das Lesen aus einer Datei ermöglicht. (Anmerkung: Wir werden uns den Klassen zur Ein- und Ausgabe in Kaptitel 12 zuwenden.) Neben den obligatorischen Methoden Read und Write, die beide synchron ausgeführt werden, werden von dieser Klasse in weiser Voraussicht des .NET-Entwicklerteams auch die asynchron operierenden Methoden BeginRead und BeginWrite veröffentlicht. Sehen wir uns die Definition der erstgenannten Methode an, die aus einem Datenstrom in ein Byte-Array einliest:


Public Overrides Function BeginRead(Byte(), Integer, _
Integer, AsyncCallback userCallback, Object) As IAsyncResult

Der Rückgabewert des Methodenaufrufs ist ein Objekt, das die Schnittstelle IAsyncResult implementiert. Der Parameter vom Typ AsyncCallback ist ein Delegat, der eine Methode im Client beschreibt, die nach der Beendigung der Leseoperation aufgerufen wird.

Sowohl BeginRead als auch BeginWrite haben jeweils eine Partnermethode: EndRead und EndWrite:


Public Overrides int EndRead(IAsyncResult) As Integer
Public Override Sub EndWrite(IAsyncResult)

Auch diese Methoden erwarten eine Referenz vom Typ IAsyncResult.

Zwei Dinge fallen sofort auf:

1. Die Methoden arbeiten gemäß Dokumentation asynchron, ohne dass im Aufrufer explizit ein separater Thread gestartet werden muss. Diese Leistung wird von den Methoden intern erbracht.
       
2. Es treten zwei Typen auf, denen Sie hier zum ersten Mal begegnen und deren Bedeutung noch unbekannt ist: IAsyncResult und AsyncCallback.
       

Wir wollen uns nun mit der Codierung einer asynchronen Ausführung beschäftigen. Danach wird auch die im ersten Moment sehr kompliziert erscheinende Parameterliste asynchron arbeitender Methoden (wie BeginRead) in einem anderen Licht erscheinen.


Galileo Computing

11.4.2 Asynchroner Methodenaufruf  downtop

Ein Delegat stellt mit BeginInvoke und EndInvoke zwei Methoden zur Verfügung, die im Rahmen einer asynchronen Operation von entscheidender Bedeutung sind. Die Methode BeginInvoke ist sehr mächtig, denn mit ihrem Aufruf auf die Referenz eines Delegaten wird ein Hintergrund-Thread erzeugt, in dem die vom Delegaten beschriebene Methode ausgeführt wird. Der aufrufende Thread macht mit seiner eigenen Arbeit weiter, anstatt auf die Beendigung der aufgerufenen Methode zu warten.

Dazu ein kleines Beispiel. Nehmen wir an, dass die Methode AsyncTestProc, die eine längere Zeit zur Ausführung benötigt, aufgerufen werden soll. AsyncTestProc sei wie folgt definiert:


Public Sub AsyncTestProc()
For i As Integer = 0 To 30
Console.Write(".X.")
Thread.Sleep(10)
Next
End Sub

Ein Client, der diese Methode asynchron ausführen möchte, kann einen Delegaten deklarieren und diesem die Adresse der Methode AsyncTestProc übergeben:


Public Delegate Sub MyDelegate()
...
Dim del As MyDelegate = New MyDelegate(AddressOf obj.AsyncTest)
del.BeginInvoke(...)

Das reicht bereits aus, um AsyncTestProc in einem separaten Thread abzuarbeiten.

Dem Aufruf von BeginInvoke müssen Argumente übergeben werden, die unsere Anweisung noch nicht enthält. Sehen wir uns deshalb nun die Definition von BeginInvoke an.


Public Function BeginInvoke([Parameterliste ,] _
AsyncCallback, Object) As IAsyncResult

Aufgerufen wird BeginInvoke auf die Instanz eines Delegaten, der auf eine bestimmte Methode zeigt. Weist die aufzurufende Methode eine Parameterliste auf, müssen die erforderlichen Argumente von BeginInvoke an die Methode weitergeleitet werden. Dazu dient die optionale Parameterliste.

Theoretisch wäre das bereits vollkommen ausreichend, um die aufgerufene Methode asynchron auszuführen. Meistens benötigt der Aufrufer aber Kenntnis von der Beendigung der asynchronen Ausführung, beispielsweise, wenn er die Rückgabewerte verarbeitet. Folglich muss es eine Möglichkeit geben, die es der asynchron aufgerufenen Methode ermöglicht, den Aufrufer davon zu unterrichten, dass sie ihre Operationen beendet hat. Dabei kann es sich nur um den Aufruf einer Methode im Initiator der asynchronen Operation handeln.

Konsequenterweise muss der asynchron aufgerufenen Methode die Adresse der Rückrufmethode im Aufrufer bekannt sein. Das klingt wieder verdächtig nach einem Delegaten – und tatsächlich ist dem so, denn dem Aufruf von BeginInvoke werden nicht nur die Argumente übergeben, welche die asynchron aufgerufene Methode benötigt, sondern darüber hinaus auch ein Objekt vom Typ AsyncCallback, bei dem es sich um den erforderlichen Delegaten handelt.

Die Definition des Delegaten AsyncCallback lautet:


Public Delegate Sub AsyncCallback(IAsyncResult)

Die Methode, die aus der asynchron ausgeführten zurückgerufen wird, hat keinen Rückgabewert und muss einen Parameter vom Typ IAsyncResult definieren.

BeginInvoke verfügt noch über einen weiteren Parameter vom Typ Object. Hier kann beim Start der asynchronen Operation ein beliebiges Objekt übergeben werden, das Informationen beliebiger Art enthält.

Das hört sich komplizierter an, als es tatsächlich ist. Daher wollen wir den Ablauf schrittweise an einem kleinen Beispiel verfolgen. Gegeben seien das Modul Module1 und die Klasse ClassA:


Module Module1
Sub Main()
...
End Sub
End Module
Class ClassA
Public Sub AsyncTest()
...
End Sub
End Class

Aus Main heraus soll die Methode AsyncTest in der Klasse ClassA asynchron aufgerufen werden. Diese Forderung bewirkt, dass wir BeginInvoke auf einen Delegaten aufrufen müssen, der die asynchron auszuführende Methode im Objekt vom Typ ClassA beschreibt. Dazu wird zunächst auf Klassenebene der Klasse Program ein Delegat mit


Public Delegate Sub MyDelegate()

deklariert. Anschließend verschaffen wir uns ein Objekt vom Typ des Delegaten, dem als Argument die asynchron auszuführende Methode übergeben wird.


Private del As MyDelegate
...
del = New MyDelegate(AddressOf obj.AsyncTest)

Mit


del.BeginInvoke(...)

wird die asynchrone Ausführung von AsyncTest in einem Hintergrund-Thread gestartet. Allerdings ist die Anweisung noch unvollständig – symbolisiert durch die Punkte. Wir sollten in Module1 nämlich noch eine Methode bereitstellen, über die der Hintergrund-Thread das Objekt über das Ende seiner Operation benachrichtigt. Die Definition der Rückrufmethode muss dem Delegaten AsyncCallback entsprechen, demnach also einen Parameter vom Typ IAsyncResult enthalten. Wir nennen diese Methode MyCallBackProc.


Module Module1
...
Sub Main()
...
End Sub
Public Sub MyCallbackProc(ar As IAsyncResult)
...
End Sub
End Module

Das Objekt vom Typ IAsyncResult entspricht dem Rückgabewert von BeginInvoke. Es veröffentlicht insgesamt sechs Eigenschaften. Dazu gehört unter anderem auch IsCompleted. Über IsCompleted kann der Aufrufer jederzeit feststellen, ob die asynchrone Ausführung bereits beendet ist. Eine zweite, sehr interessante Eigenschaft ist AsyncState, die genau das Objekt abruft, das als letzter Parameter dem Aufruf von BeginInvoke übergeben worden ist. Sie werden später in einem anderen Beispiel die sinnvolle Auswertung dieses Objekts sehen.

Wir wollen nun unser Beispiel komplettieren und sowohl innerhalb des Servers als auch innerhalb des Clients Code einsetzen, der tatsächlich einige Zeit in Anspruch nimmt, damit wir den Effekt des asynchronen Aufrufs tatsächlich beobachten können.


' ----------------------------------------------------------
' Beispiel: ...\Kapitel 11\AsynchronerAufruf_1
' ----------------------------------------------------------
Imports System.Threading
Module Module1
Public Delegate Sub MyDelegate()
Private del As MyDelegate
Sub Main()
Dim obj As New ClassA
' Delegat, der die asynchron aufzurufende Methode be-schreibt
del = New MyDelegate(AddressOf obj.AsyncTest)
' Delegat vom Typ AsyncCallback beschreibt die Metho-de, die
' der Server nach Beendigung der asynchronen Ausführung aufruft
Dim callback As AsyncCallback = _
New AsyncCallback(AddressOf MyCallbackProc)
' die Methode AsyncTest in ClassA asynchron aufrufen
del.BeginInvoke(callback, Nothing)
' zeitaufwändige Ausführung
Dim i As Integer
For i = 0 To 100
Console.Write(".P.")
Thread.Sleep(10)
Next
Console.ReadLine()
End Sub
' die zurückgerufene Methode
Public Sub MyCallbackProc(ByVal ar As IAsyncResult)
Console.Write("Ich habe fertig.")
End Sub
End Module
Class ClassA
' asynchron aufzurufende Methode
Public Sub AsyncTest()
' zeitintensive Ausführung
Dim i As Integer
For i = 0 To 30
Console.Write(".X.")
Thread.Sleep(10)
Next
End Sub
End Class

In der Abbildung 11.10 ist das Ergebnis des Aufrufs zu sehen. Es ist eindeutig zu erkennen, dass .P. bzw. .X. mehr oder weniger abwechselnd ausgegeben werden, denn beide Methoden arbeiten parallel. Beendet wird die asynchrone Operation durch den Rückruf von MyCallbackProc, was durch die Ausgabe des bekannten Satzes »Ich habe fertig« bestätigt wird.

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 11.10     Ausgabe eines asynchronen Aufrufs

Beispielprogramm – Pumpen asynchron einschalten

Das Beispielprogramm der Pumpenschaltung aus Kapitel 7 zeigte, wie die Pumpen mittels Rückruf den Client über das Einschalten der Motoren in Kenntnis setzten. In diesem Beispiel wurden die Pumpen der Reihe nach eingeschaltet. Dabei wurde jedoch nicht berücksichtigt, dass der Startvorgang durchaus eine gewisse Zeit in Anspruch nehmen kann. Jetzt soll das Programm so abgeändert werden, dass die Einschaltvorgänge nicht mehr hintereinander, sondern gleichzeitig erfolgen.


' ----------------------------------------------------------
' Beispiel: ...\Kapitel 11\PumpenEinschalten
' ----------------------------------------------------------
Imports System.Threading
Public Delegate Sub PumpDelegate()
Class Client
Public Shared Sub Main()
Dim myClient As New Client
' Delegat auf die von den Pumpen zurückzurufende Methode
Dim callback As AsyncCallback = _
New AsyncCallback(AddressOf myClient.PumpInfo)
' Steuerklasse instanziieren
Dim obj As ControlPumps = New ControlPumps(callback)
' Pumpen bereitstellen
Dim p1 As PumpeA = New PumpeA("Nummer 1")
obj.AddPump(p1)
Dim p2 As PumpeA = New PumpeA("Nummer 2")
obj.AddPump(p2)
Dim p3 As PumpeA = New PumpeA("Nummer 3")
obj.AddPump(p3)
' Pumpen starten
obj.StartAllPumps()
' Simulation von Clientoperationen
Dim i As Integer
For i = 0 To 9
Console.WriteLine("ich warte...")
Thread.Sleep(1000)
Next
Console.ReadLine()
End Sub
Public Sub PumpInfo(ByVal ar As IAsyncResult)
Console.WriteLine("Die Pumpe {0} ist angelaufen.", _
ar.AsyncState)
End Sub
End Class
' ---------------- ControlPumps-Klasse -----------------
Public Delegate Sub CallbackDelegate(ByVal ar As IAsyncResult)
Public Class ControlPumps
Private colPumps As New ArrayList
Private callback As AsyncCallback
' Konstruktor
Public Sub New(ByVal callback As AsyncCallback)
Me.callback = callback
End Sub
' Pumpen der Collection hinzufügen
Public Sub AddPump(ByVal pObj As PumpeA)
colPumps.Add(pObj)
End Sub
' alle Pumpen der Reihe nach einschalten
Public Sub StartAllPumps()
' die Liste der Pumpen durchlaufen
Dim pumpe As PumpeA
For Each pumpe In colPumps
' Delegat auf die Startmethode
Dim delPump As PumpDelegate = _
New PumpDelegate(AddressOf pumpe.SwitchOnA)
' Startmethode asynchron aufrufen
delPump.BeginInvoke(callback, pumpe.Bezeichner)
Next
End Sub
End Class
' ---------------- PumpeA-Klasse -----------------
Public Class PumpeA
Private myBezeichner As String
' Konstruktor
Public Sub New(ByVal bezeichner As String)
Me.myBezeichner = bezeichner
End Sub
' Eigenschaftsmethode
Public ReadOnly Property Bezeichner() As String
Get
Return Me.myBezeichner
End Get
End Property
' Methode zum Einschalten
Public Sub SwitchOnA()
Dim rnd As New Random
Dim dauer As Integer = rnd.Next(100, 5000)
Thread.Sleep(dauer)
End Sub
End Class

In diesem Beispielprogramm werden nicht die Delegaten auf die Startmethoden an die steuernde Klasse übergeben, sondern die Referenzen auf die Pumpen. Damit wir später die Pumpen identifizieren können, die sich bei der Rückrufmethode nach dem erfolgreichen Starten melden, ist die Pumpenklasse um einen Konstruktor und die Eigenschaft Bezeichner ergänzt worden. Die Eigenschaft liefert den Inhalt des Feldes bezeichner zurück.

Weil alle Pumpen dieselbe Methode im Client zurückrufen, bietet es sich an, der Steuerklasse den Delegaten auf diese Methode zu übergeben:


Dim callback As AsyncCallback = _
New AsyncCallback(AddressOf myClient.PumpInfo)
Dim obj As ControlPumps = New ControlPumps(callback)

Die Referenz auf den übergebenen Delegaten hält das Objekt der Klasse ControlPumps in einer privaten Variablen vor.

Nachdem alle Pumpen in der Steuerklasse registriert sind, können sie durch den Aufruf von StartAllPumps im Client gestartet werden. In einer For Each-Schleife werden alle in der internen Auflistung gespeicherten Pumpenobjekte durchlaufen und für jedes Objekt ein Delegat erzeugt, der auf die objektspezifische Startmethode zeigt. Mit


delPump.BeginInvoke(callback, pumpe.Bezeichner)

wird anschließend BeginInvoke auf diesem Delegaten aufgerufen. Damit gewährleisten wir, dass die Startmethode der jeweiligen Pumpe in einem eigenen Thread ausgeführt wird. Das Besondere an diesem Aufruf ist, dass wir dem zweiten Parameter ein Zustandsobjekt übergeben – es handelt es dabei um die Zeichenfolge, die eine Pumpe eindeutig beschreibt.

In der Rückrufmethode des Clients kann das dem BeginInvoke-Aufruf übergebene Zustandsobjekt, das von der Steuerklasse an den Thread weitergereicht und von der Rückrufmethode übergeben wird, ausgewertet werden. Dazu dient die Eigenschaft AsyncState des IAsyncResult-Parameters. Damit wissen wir nicht nur, dass eine Pumpe angelaufen ist, sondern gleichzeitig auch, um welche es sich handelt.


Public Sub PumpInfo(ByVal ar As IAsyncResult)
Console.WriteLine("Die Pumpe {0} ist angelaufen.", _
ar.AsyncState)
End Sub


Galileo Computing

11.4.3 Asynchroner Aufruf mit Rückgabewerten  downtop

Möglicherweise liefert die asynchrone Methode als Resultat ihrer Operation einen Rückgabewert. Vielleicht werden auch über die Parameterliste Ergebnisse bereitgestellt. Wird aus dem Hintergrund-Thread heraus die Rückrufmethode des Initiators der asynchronen Operation aufgerufen, stehen die Ergebnisse jedoch nicht automatisch zur Verfügung, sie müssen ausdrücklich abgerufen werden. Dazu dient die Methode EndInvoke des Delegaten.


Public Function EndInvoke([Parameterliste,] IAsyncResult) As Datentyp

Wie bei BeginInvoke müssen Sie auch EndInvoke eine vorgeschriebene Parameterliste übergeben, die nicht identisch mit der Parameterliste von BeginInvoke ist: Sie darf nur die Referenzparameter der asynchronen Methode enthalten, damit diese ihre Resultate dort hineinschreiben kann. Die Angabe der Werteparameter ist nicht erlaubt. Der einzige grundsätzlich immer zwingend erforderliche Parameter ist vom Typ IAsyncResult. Hier wird das Objekt übergeben, das die Rückrufmethode des Clients vom Server erhalten hat.

Wir wollen nun das Beispiel AsynchronerAufruf_1 ändern, um zu sehen, wie eine asynchrone Methode behandelt wird, die sowohl Werte- als auch Referenzparameter erwartet und darüber hinaus auch noch einen Rückgabewert hat. Dazu implementieren wir die Methode AsyncTest wie folgt:


Public Function AsyncTest(ByVal x As Integer, _
ByRef y As Long) As String
' zeitaufwändige Ausführung
Dim i As Integer
For i = 0 To 30
Console.Write(".X.")
Thread.Sleep(10)
Next
y = 12345
Return "Ich habe fertig."
End Function

Die Parameterliste enthält jetzt den Referenzparameter y und den Werteparameter x, außerdem liefert die Methode eine Zeichenfolge zurück.

Die Änderung der Signatur hat natürlich auch im auslösenden Thread Konsequenzen. Der Delegat, der den Aufruf der Methode kapselt, muss an die veränderten Bedingungen angepasst werden:


Public Delegate Function MyDelegate(ByVal x As Integer, _
ByRef y As Long) As String

Gleiches gilt auch für den Start der asynchronen Bearbeitung, denn nun reicht es nicht mehr aus, mit BeginInvoke einfach nur einen Delegaten auf die Rückrufmethode zu übergeben sowie die Referenz auf ein Objekt, in das der asynchrone Aufruf Informationen schreiben könnte. Wir müssen stattdessen auch die Parameter der asynchronen Methode in der richtigen Reihenfolge bedienen:


del.BeginInvoke(intVar, lngVar, callback, Nothing)

AsyncTest nimmt nun eine Kopie des Integer-Wertes und die Adresse des Long-Wertes entgegen, kann mit diesen die erforderlichen Operationen ausführen und zum Abschluss durch Aufruf der über callback bekannt gegebenen Adresse die Methode MyCallbackProc informieren.

Der Implementierung der Rückrufmethode kommt nun eine entscheidende Bedeutung zu. Es gilt, sowohl den Rückgabewert als auch den in diesem Fall geänderten Inhalt der Variablen lngVar auszuwerten. Dem Aufruf von EndInvoke übergeben wir die Adresse von lngVar und holen uns den Rückgabewert an der Konsole ab:


Public Sub MyCallbackProc(ByVal ar As IAsyncResult)
Dim lngVar As Long
' zeigt den Rückgabewert der asynchronen Methode an
Console.Write(del.EndInvoke(lngVar, ar))
' schreibt den Inhalt des Referenzparameters lngVar
Console.Write("..Wert y = {0}", lngVar)
End Sub

Die Konsolenausgabe bestätigt, dass unser Unterfangen von Erfolg beschieden ist: Wir erhalten sowohl die Zeichenfolge als auch den veränderten Inhalt des Feldes intVar.

Zum Abschluss fassen wir das Beispielprogramm noch einmal zusammen.


' ----------------------------------------------------------
' Beispiel: ...\Kapitel 11\AsynchronerAufruf_2
' ----------------------------------------------------------
Imports System.Threading
Module Module1
Public Delegate Function MyDelegate(ByVal x As Integer, _
ByRef y As Long) As String
Private del As MyDelegate
Sub Main()
Dim obj As New ClassA
' Delegat, der die asynchron aufzurufende Methode beschreibt
del = New MyDelegate(AddressOf obj.AsyncTest)
' Delegat vom Typ AsyncCallback beschreibt die Methode, die
' der Server nach Beendigung der asynchronen Ausführung aufruft
Dim callback As AsyncCallback = _
New AsyncCallback(AddressOf MyCallbackProc)
' die Methode AsyncTest in ClassA asynchron aufrufen
Dim intVar As Integer = 34
Dim lngVar As Long = 3000
del.BeginInvoke(intVar, lngVar, callback, Nothing)
' zeitaufwändige Ausführung
Dim i As Integer
For i = 0 To 100
Console.Write(".P.")
Thread.Sleep(10)
Next
Console.ReadLine()
End Sub
' die zurückgerufene Methode
Public Sub MyCallbackProc(ByVal ar As IAsyncResult)
Dim lngVar As Long
' zeigt den Rückgabewert der asynchronen Methode an
Console.Write(del.EndInvoke(lngVar, ar))
' schreibt den Inhalt des Referenzparameters lngVar
Console.Write("..Wert y = {0}", lngVar)
End Sub
End Module
Class ClassA
' asynchron aufzurufende Methode
Public Function AsyncTest(ByVal x As Integer, _
ByRef y As Long) As String
' zeitaufwändige Ausführung
Dim i As Integer
For i = 0 To 30
Console.Write(".X.")
Thread.Sleep(10)
Next
y = 12345
Return "Ich habe fertig."
End Function
End Class


Galileo Computing

11.4.4 Eine Klasse mit asynchronen Methodenaufrufen  toptop

Am Anfang dieses Abschnitts wurde schon darauf hingewiesen, dass einige Klassen der .NET-Klassenbibliothek Methoden mit asynchroner Verarbeitung anbieten. Die Klasse FileStream im Namespace System.IO ist ein Beispiel dafür. Es werden allerdings nicht die Methoden BeginInvoke und EndInvoke aufgerufen, sondern zwei ähnlich lautende: BeginRead und EndRead bzw. BeginWrite und EndWrite.

Wir wollen uns nun ansehen, wie eine Klasse aufgebaut ist, die ähnlich wie FileStream implementiert ist. Dabei lernen wir einerseits, wie wir die asynchronen Methoden der Klassen des .NET Frameworks behandeln müssen, andererseits aber auch, diese Technik in eigenen Klassen zu nutzen.

Am Anfang steht die Idee, eine Methode zu entwickeln, von der wir annehmen, dass sie in Abhängigkeit von den Umgebungsbedingungen und der Art der Operation eine längere Zeit zur Bearbeitung in Anspruch nehmen kann. Wir wollen diese Methode nachfolgend Calculate nennen, die Klasse dazu Server.


Class Server
Public Function Calculate(ByVal x As Integer) As Integer
Console.Write("---Bearbeitung startet---")
Dim i As Integer
For i = 0 To 20
Console.Write(".X.")
Thread.Sleep(10)
Next
Console.Write("---Bearbeitung beendet---")
Return x * x
End Function
End Class

Die For-Schleife simuliert eine länger andauernde Operation. Diese Implementierung arbeitet synchron. Da wir uns bewusst sind, dass Calculate vielleicht auch eine Stunde zur vollständigen Ausführung brauchen könnte (wir sind mit unserer Annahme sehr großzügig), bieten wir zusätzlich eine asynchrone Variante an. Dazu benötigen wir zwei weitere Methoden, die einer allgemeinen Konvention folgend als BeginXxx und EndXxx bezeichnet werden – in unserer Klasse demnach BeginCalculate und EndCalculate. Die noch unvollständige Klassenstruktur sieht dann folgendermaßen aus:


Class Server
' Methode Calculate wird synchron ausgeführt
Public Function Calculate(ByVal x As Integer) As Integer
...
End Function
' Start der asynchronen Ausführung
Public Function BeginCalculate(ByVal intVar As Integer, _
ByVal asyncCallback As AsyncCallback, _
ByVal state As Object) As IAsyncResult
...
End Function
' Beenden der asynchronen Ausführung
Public Function EndCalculate(ByVal asyncResult As IAsyncResult) _
As Integer
...
End Function
End Class

An dieser Stelle kommt es zu der wichtigsten Entscheidung überhaupt. Was wir beabsichtigen, ist die asynchrone Ausführung der Methode Calculate. Asynchronität heißt aber auch, dass ein weiterer Thread gestartet werden muss, sobald die Methode BeginCalculate aufgerufen wird. Wenn wir in dieser Methode ein Objekt vom Typ Thread erzeugen und seinem Konstruktor einen Delegaten übergeben, bräuchten wir auch noch ein Objekt, welches das Interface IAsyncResult implementiert, müssten zwangsläufig dessen Methoden implementieren usw.

Die Entwicklung auf diese Weise zu gestalten, ist sehr aufwändig. Es gibt eine viel einfachere Lösung, da die beiden Methoden BeginInvoke und EndInvoke genau das leisten, was wir brauchen. Also benutzen wir sie auch, um das Ziel effizient zu erreichen. Dazu wird die Logik, die in den Abschnitten 11.4.2 und 11.4.3 beschrieben wurde, innerhalb der Klasse Server implementiert.


' ----------------------------------------------------------
' Beispiel: ...\ Kapitel 11\AsynchronerAufruf_3
' ----------------------------------------------------------
Imports System.Threading
Class Server
' Deklaration eines Delegaten, der den Funktionsaufruf
' von 'Caluculate' beschreibt
Public Delegate Function CalculateHandler(ByVal x As Integer) _
As Integer
Dim del As CalculateHandler
' Methode Calculate wird synchron ausgeführt
Public Function Calculate(ByVal x As Integer) As Integer
Console.Write("---Bearbeitung startet---")
Dim i As Integer
For i = 0 To 20
Console.Write(".X.")
Thread.Sleep(10)
Next
Console.Write("---Bearbeitung beendet---")
Return x * x
End Function
' Start der asynchronen Ausführung
Public Function BeginCalculate(ByVal intVar As Integer, _
ByVal asyncCallback As AsyncCallback, _
ByVal state As Object) As IAsyncResult
del = New CalculateHandler(AddressOf Calculate)
' Aufruf der Methode Calculate, die in einem eigenen
' Thread ausgeführt wird
Return del.BeginInvoke(intVar, asyncCallback, Nothing)
End Function
' Beenden der asynchronen Ausführung
Public Function EndCalculate(ByVal asyncResult As IAsyncResult) _
As Integer
Return del.EndInvoke(asyncResult)
End Function
End Class

Dem Aufruf der Methode BeginCalculate werden die Daten übergeben, welche die Methode Calculate für ihre Operation benötigt. In unserem Beispiel handelt es sich nur um einen als Werteparameter deklarierten Integer. Der zweite Parameter erhält die Referenz auf einen Delegaten, der die Rückrufmethode im Aufrufer beschreibt. Der dritte und letzte Parameter dient dazu, ein Objekt bereitzustellen, mit dem Daten zwischen dem aufrufenden und dem aufgerufenen Objekt auszutauschen. Ein solches Objekt ist in unserem Beispielcode nicht vorgesehen.

Der Aufruf von BeginCalculate orientiert sich an dem von BeginInvoke – und das ist typisch für Klassen im .NET Framework, die asynchrone Methoden offen legen. Unter ähnlicher Prämisse wird auch EndCalculate implementiert, der Rückgabewert des internen EndInvoke-Aufrufs wird zum Rückgabewert der Instanzmethode.

Es bleibt zum Schluss noch zu testen, ob die Klassenimplementierung auch unseren Anforderungen genügt. Dazu entwickeln wir einen Client mit der Methode Start zum Aufruf der asynchronen Ausführung und einer Methode Results, die als Rückruffunktion vom Server angesteuert wird.


Class Client
Private myObj As New Server
Shared Sub Main()
Dim myClient As New Client
myClient.Start()
End Sub
Public Sub Start()
Dim iVar As Integer = 23
Dim callback As AsyncCallback = _
New AsyncCallback(AddressOf Me.Results)
' Aufruf der asynchronen Ausführung
Dim ia As IAsyncResult = _
myObj.BeginCalculate(iVar, callback, Nothing)
Dim i As Integer
For i = 0 To 100
Console.Write(".{0}.", i)
Thread.Sleep(5)
Next
Console.ReadLine()
End Sub
' diese Methode wird vom Server aufgerufen
Public Sub Results(ByVal asyncResult As IAsyncResult)
' das Ergebnis der asynchronen Operation abholen
Dim res As Integer = myObj.EndCalculate(asyncResult)
Console.Write("---Resultat = {0} ", res)
Console.Write("---FERTIG---")
End Sub
End Class

Die Ausgabe an der Konsole wird wie in der folgenden Abbildung gezeigt aussehen.

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 11.11     Ausgabe des Beispiels »AsynchronerAufruf_3«


Hinweis

Sie sollten grundsätzlich immer, wenn die Methode einer Klasse eine länger andauernde Operation ausführt, neben der asynchronen Variante auch die synchron arbeitende Methode anbieten, um dem Benutzer die Entscheidung zu überlassen, ob er die synchrone oder asynchrone Variante aufrufen möchte.


 

 <<   zurück
  
  Zum Katalog
Zum Katalog: Visual Basic 2005
Visual Basic 2005
bestellen
 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchtipps
Zum Katalog: Visual C# 2005






 Visual C# 2005


Zum Katalog: Fortgeschrittene Programmierung mit Visual C# 2005






 Fortgeschrittene
 Programmierung
 mit Visual C# 2005


Zum Katalog: Das Programmierhandbuch SQL Server 2005






 Das Programmier-
 handbuch
 SQL Server 2005


Zum Katalog: Einstieg in Visual Basic 2005






 Einstieg in
 Visual Basic 2005


Zum Katalog: Einstieg in Visual C# 2005






 Einstieg in
 Visual C# 2005


Zum Katalog: Konzepte und Lösungen für Microsoft-Netzwerke






 Konzepte und
 Lösungen für
 Microsoft-Netzwerke


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo








Copyright © Galileo Press 2007
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de